#region License
/*
 * NReco library (http://nreco.googlecode.com/)
 * Copyright 2008,2009 Vitaliy Fedorchenko
 * Distributed under the LGPL licence
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Microsoft.CSharp;
using NReco.Converting;

namespace NReco.Composition {
	
	/// <summary>
	/// Evaluates C# code using internal .NET on-fly compilation mechanizm.
	/// Code evaluated in context of autogenerated local variables.
	/// </summary>
	public class EvalCsCode : IProvider<object,object>, IOperation<object> {
		/// <summary>
		/// List of assemblies that always are referenced while code compiling.
		/// </summary>
		static string[] BasicAssemblies = new string[] {
			"System.dll","System.Xml.dll", "System.Data.dll", @"NReco.dll"};
		static string[] BasicNamespaces = new string[] {
			"System", "System.IO", "System.Text", "System.Text.RegularExpressions",
			"System.Collections.Generic", "System.Collections", "NReco" };
		static string CodeNamespace = "NReco.Generated";
		static string CodeClassName = "CodeContainer";
		static string CodeMethodName = "Run";
		static string CodeTemplate = @"
			{5}
			namespace {0} {{ 
				public class {1} {{
					public object {2} (NReco.NameValueContext context) {{
						object result = null;
						{3}
						{4}
						return result;
					}}
				}}
			}}";
		static IDictionary<string,Assembly> AssemblyCache = new Dictionary<string,Assembly>();
		
		string _Code;
		string[] _ExtraAssemblies = null;
		string[] _ExtraNamespaces = null;
		VariableDescriptor[] _Variables = null;

		/// <summary>
		/// C# code to evaluate. Required.
		/// </summary>
		public string Code {
			get { return _Code; }
			set { _Code = value; }
		}

		/// <summary>
		/// List of extra assemblies used by specified C# code. Optional.
		/// </summary>
		public string[] ExtraAssemblies {
			get { return _ExtraAssemblies; }
			set { _ExtraAssemblies = value; }
		}

		/// <summary>
		/// List of extra namespaced used by specified C# code. Optional.
		/// </summary>
		public string[] ExtraNamespaces {
			get { return _ExtraNamespaces; }
			set { _ExtraNamespaces = value; }
		}

		/// <summary>
		/// List of variable definitions used by specified C# code. Optional.
		/// </summary>
		public VariableDescriptor[] Variables {
			get { return _Variables; }
			set { _Variables = value; }
		}

		protected void GenerateUsing(StringBuilder sb, string ns) {
			sb.Append("using ");
			sb.Append(ns);
			sb.Append(";\n");
		}
		protected string GetCsTypeName(Type t) {
			if (t.IsGenericType) {
				string genTypeDefName = t.GetGenericTypeDefinition().FullName;
				int genSepIdx = genTypeDefName.IndexOf("`");
				if (genSepIdx>=0)
					genTypeDefName = genTypeDefName.Substring(0, genSepIdx);

				Type[] genTypeArgs = t.GetGenericArguments();
				string[] genTypeNames = new string[genTypeArgs.Length];
				for (int i=0; i<genTypeNames.Length; i++)
					genTypeNames[i] = GetCsTypeName(genTypeArgs[i]);
				return String.Format("{0}<{1}>", genTypeDefName, String.Join(",",genTypeNames));
			}
			return t.FullName;
		}
		protected void GenerateLocalVar(StringBuilder sb, VariableDescriptor varDescr) {
			string typeName = GetCsTypeName(varDescr.VarType);
			sb.AppendFormat("{0} {1} = ({0})context[\"{1}\"];", typeName,varDescr.Name);
		}

		protected object GetCodeObject() {
			// prepare code
			StringBuilder usingCodeBuilder = new StringBuilder();
			foreach (string ns in BasicNamespaces)
				GenerateUsing(usingCodeBuilder, ns);
			if (ExtraNamespaces != null)
				foreach (string ns in ExtraNamespaces)
					GenerateUsing(usingCodeBuilder, ns);
			StringBuilder varContextBuilder = new StringBuilder();
			foreach (VariableDescriptor varDescr in Variables) {
				GenerateLocalVar(varContextBuilder, varDescr);
			}
			string evalCode = Code.Trim();
			if (!evalCode.EndsWith(";")) evalCode += ";";
			string generatedCode = String.Format(CodeTemplate,
					CodeNamespace, CodeClassName, CodeMethodName,
					varContextBuilder, evalCode, usingCodeBuilder.ToString());
			
			Assembly assembly = null;
			lock (AssemblyCache) {
				// concurrent compilation should not be allowed to avoid 'phantom' assemblies for same code
				if (AssemblyCache.ContainsKey(generatedCode)) {
					assembly = AssemblyCache[generatedCode];
				} else {
					List<string> refAssemblies = new List<string>(BasicAssemblies);
					if (ExtraAssemblies != null)
						foreach (string extraAssm in ExtraAssemblies)
							if (!refAssemblies.Contains(extraAssm))
								refAssemblies.Add(extraAssm);
					IProvider<string, Assembly> codeAssemblyPrv = new CompileCodeAssembly(
						new CSharpCodeProvider(), refAssemblies.ToArray());
					assembly = codeAssemblyPrv.Provide(generatedCode);
					AssemblyCache[generatedCode] = assembly;
				}
			}
			return assembly.CreateInstance(CodeNamespace+"."+CodeClassName);
		}
		
		public object Provide(object context) {
			object o = GetCodeObject();
			Type t = o.GetType();
			MethodInfo mi = t.GetMethod(CodeMethodName);

			NameValueContext varContext = new NameValueContext();
			foreach (VariableDescriptor varDescr in Variables) {
				object varValue = varDescr.VarProvider.Provide(context);
				if (varValue != null && !varDescr.VarType.IsInstanceOfType(varValue)) {
					ITypeConverter cnv = ConvertManager.FindConverter( varValue.GetType(), varDescr.VarType );
					if (cnv!=null)
						varValue = cnv.Convert(varValue,varDescr.VarType);
					else
						throw new InvalidCastException(
								String.Format("Context variable {0} cannot be cast to expected type {1}",
									varDescr.Name, varDescr.VarType));
				}
				varContext[varDescr.Name] = varValue;
			}

			object s = mi.Invoke(o, new object[] { varContext } );
			return s;
		}

		public void Execute(object context) {
			Provide(context);
		}

		public class VariableDescriptor {

			public string Name { get; private set; }
			public Type VarType { get; private set;  }
			public IProvider<object, object> VarProvider { get; private set; }

			public VariableDescriptor(string name, Type varType, IProvider<object, object> varPrv) {
				Name = name;
				VarType = varType;
				VarProvider = varPrv;
			}

		}

	}
}
